package com.sc.nft.wrapper;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.extra.servlet.ServletUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.sc.nft.annotation.UserLogin;
import com.sc.nft.annotation.WithoutSign;
import com.sc.nft.config.AuthProperties;
import com.sc.nft.constant.Constants;
import com.sc.nft.dao.UserInfoDao;
import com.sc.nft.dao.WxMiniUserInfoDao;
import com.sc.nft.entity.OperationLog;
import com.sc.nft.entity.UserInfo;
import com.sc.nft.entity.WxMiniUserInfo;
import com.sc.nft.exception.GlobalException;
import com.sc.nft.exception.GlobalRunTimeException;
import com.sc.nft.pool.UserPool;
import com.sc.nft.service.DeviceNoBackListService;
import com.sc.nft.sup.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author zhangxiangyu
 * @date 2021年3月25日
 */
@Slf4j
@Component("localUserAuthInterceptor") // 与基础组件中存在同名类，规避Bean命名冲突
public class UserAuthInterceptor implements HandlerInterceptor {
    @Resource
    UserInfoDao userInfoDao;

    @Resource
    RedisTemplate redisTemplate;

    @Resource
    private AuthProperties authProperties;
    @Resource
    private DeviceNoBackListService deviceNoBackListService;


    @Resource
    WxMiniUserInfoDao wxMiniUserInfoDao;

    public static final String API_SIGN_NONCE_SET = "api_sign_nonce_set";
    private static final String SECRET =
            "Z7Ds^C8vTjwWRBWk06yFzp0xn9cfBo$Jdl6$&2uPKyNmKaBmRWTtRz%1Sy5KMa5z";

    private ThreadLocal<String> params = new ThreadLocal<>();
    private ThreadLocal<Long> timeCount = new ThreadLocal<>();

    private final List<String> fileParamsUrl = Lists.newArrayList(
            "/user/faceVerification"
    );

    @Override
    public boolean preHandle(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            Object handler)
            throws Exception {
        timeCount.set(System.currentTimeMillis());
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        String deviceNo = httpServletRequest.getHeader("deviceNo");
        if (ObjectUtil.isNotNull(deviceNo)) {
            Assert.isFalse(deviceNoBackListService.checkByDeviceNo(deviceNo), () -> new GlobalException("设备已被封禁"));
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (!fileParamsUrl.contains(httpServletRequest.getRequestURI())) {
            params.set(new RequestWrapper(httpServletRequest).getBody());
        }

        if (!method.isAnnotationPresent(WithoutSign.class)) {
            if (Boolean.TRUE.equals(authProperties.getOpen())) {
                this.validateSign(httpServletRequest);
            }
        }
        // 检查有没有需要用户权限的注解
        if (!method.isAnnotationPresent(UserLogin.class)) {
            return true;
        }
        String token = httpServletRequest.getHeader("token"); // 从 http 请求头中取出 token
        //获取微信用户登录凭证
        String openid = httpServletRequest.getHeader("openId");
//        String xtraceId = httpServletRequest.getHeader("X-Trace-Id");
//        log.info("X-Trace-Id：{}，header取出的值为：{}",xtraceId,token);
        UserLogin memberLoginToken = method.getAnnotation(UserLogin.class);
        if (memberLoginToken.required()) {
            // 执行认证
            if (StrUtil.isBlank(token) && StrUtil.isBlank(openid)) {
                throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
            }

            boolean anyTrue = false;    // 标记Token登录和微信登录至少有一个存在

            // Token登录
            if (StpUtil.isLogin()) {
                long userId = StpUtil.getLoginIdAsLong();
                UserInfo userInfo = userInfoDao.getById(userId);
//                log.info("用户id：{},用户是否存在：{}",userId,ObjectUtil.isNotNull(userInfo));
                if (userInfo == null) {
                    throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
                }
                if (userInfo.getIsFreeze()) {
                    throw new GlobalException(ErrorCode.ACCOUNT_HAS_BEEN_FROZEN);
                }
                UserPool.setUser(userInfo);
                anyTrue = true;
            }

            // 微信登录
            String miniUserId = "";
            if (StrUtil.isNotBlank(openid)) {
                miniUserId = (String) redisTemplate.opsForValue().get(openid);
            }
            if (StrUtil.isNotBlank(miniUserId)) {
                WxMiniUserInfo wxMiniUserInfo = wxMiniUserInfoDao.getById(Long.parseLong(miniUserId));
                if (wxMiniUserInfo == null) {
                    throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
                }
                if (!wxMiniUserInfo.getIsOpen()) {
                    throw new GlobalException(ErrorCode.ACCOUNT_HAS_BEEN_FROZEN);
                }
                UserPool.setWxUser(wxMiniUserInfo);
                anyTrue = true;
            }

            // 两者都不存在表示未登录
            if (!anyTrue) {
//                log.info("登录校验结果：{}",anyTrue);
                throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
            }

        } else {

            if (StrUtil.isNotBlank(token)) {
                if (!StpUtil.isLogin()) {
                    throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
                }
                long userId = StpUtil.getLoginIdAsLong();
                UserInfo userInfo = userInfoDao.getById(userId);
                if (userInfo != null) {
                    UserPool.setUser(userInfo);
                }
            }
            if (StrUtil.isNotBlank(openid)) {
                String miniUserId = (String) redisTemplate.opsForValue().get(openid);
                if (StrUtil.isBlank(miniUserId)) {
                    throw new GlobalException(ErrorCode.NO_LOGIN_ERROR);
                }
                WxMiniUserInfo wxMiniUserInfo = wxMiniUserInfoDao.getById(Long.parseLong(miniUserId));
                if (wxMiniUserInfo != null) {
                    UserPool.setWxUser(wxMiniUserInfo);
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            Object o,
            ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            Object handler,
            Exception e) {
        try {
            OperationLog operationLog = new OperationLog();
            operationLog.setOperationTime(new Date());
            operationLog.setRequestMethod(httpServletRequest.getMethod());
            if (StrUtil.equals(operationLog.getRequestMethod(), "GET")) {
                operationLog.setRequestParams(JSON.toJSONString(httpServletRequest.getParameterMap()));
            } else {
                operationLog.setRequestParams(params.get());
            }
            operationLog.setRequestUrl(httpServletRequest.getRequestURI());
            operationLog.setRequestResult(InterceptResponse.resultBody.get());
            operationLog.setUserId(UserPool.getUser() != null ? UserPool.getUser().getId() : null);
            operationLog.setRunTime(System.currentTimeMillis() - timeCount.get());
            operationLog.setIp(ServletUtil.getClientIP(httpServletRequest));
            operationLog.setDeviceNo(httpServletRequest.getHeader("deviceNo"));
            operationLog.setMacNo(httpServletRequest.getHeader("macNo"));
            operationLog.setPhoneModel(httpServletRequest.getHeader("phoneModel"));
            operationLog.setVersion(httpServletRequest.getHeader("version"));
            operationLog.insert();
        } catch (Exception e1) {
            log.error("error:{}", e1.getMessage(), e1);
            log.warn("save operation log fail");
        } finally {
            UserPool.clear();
            params.set(null);
            params.remove();
            timeCount.set(null);
            timeCount.remove();
            InterceptResponse.resultBody.set(null);
            InterceptResponse.resultBody.remove();
        }
    }


    private void validateSign(HttpServletRequest request) {
        String nonce = request.getHeader("nonce");
        if (StringUtils.isBlank(nonce)) {
            throw new GlobalRunTimeException(ErrorCode.SIGN_INVALID);
        }
        Boolean flag = redisTemplate.hasKey(API_SIGN_NONCE_SET + nonce);
        if (Boolean.TRUE.equals(flag)) {
            throw new GlobalRunTimeException(ErrorCode.SIGN_INVALID);
        }
        String md5 = DigestUtil.md5Hex(DigestUtil.md5Hex(nonce));
        String time = request.getHeader("timestamp");
        String sign = request.getHeader("sign");
        log.debug("=========接收到的nonce为{}，timestamp为{}，sign为{}", nonce, time, sign);
        if (StringUtils.isBlank(time)) {
            throw new GlobalRunTimeException(ErrorCode.NO_TIMESTAMP);
        }
        if (!StringUtils.isNumeric(time)) {
            throw new GlobalRunTimeException(ErrorCode.NO_TIMESTAMP);
        }
        long timestamp = Long.parseLong(time);
        long now = System.currentTimeMillis();
        long seconds = authProperties.getExpireMinutes() * 60;
        if (Math.abs(now - timestamp) > seconds * 1000) {
            throw new GlobalRunTimeException(ErrorCode.TIMESTAMP_INVALID);
        }
        HMac mac = new HMac(HmacAlgorithm.HmacSHA256, SECRET.getBytes());
        String correct = Base64.encode(mac.digestHex("nonce=" + md5 + "&timestamp=" + time));
        log.debug("==============正确签名为：" + correct);
        if (!sign.equals(correct)) {
            throw new GlobalRunTimeException(ErrorCode.SIGN_INVALID);
        }
        redisTemplate.opsForValue().set(API_SIGN_NONCE_SET + nonce, "1", 1L, TimeUnit.MINUTES);
    }

    private Map<String, Object> get(HttpServletRequest request) {
        Enumeration<String> params = request.getParameterNames();
        Map<String, Object> result = new HashMap<>();
        while (params.hasMoreElements()) {
            String param = params.nextElement();
            result.put(param, request.getParameter(param));
        }
        return result;
    }

    private Map<String, Object> post(HttpServletRequest request) {
        String contentType = request.getContentType();
        StringBuilder sb;
        try {
            BufferedReader streamReader =
                    new BufferedReader(
                            new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
            String inputStr;
            sb = new StringBuilder();
            while ((inputStr = streamReader.readLine()) != null) {
                sb.append(inputStr);
            }
        } catch (IOException e) {
            log.error("transfer post data failed,error: ", e);
            throw new GlobalRunTimeException(ErrorCode.SYS_ERROR);
        }
        if (sb.length() == 0) {
            log.error("params has no param");
            throw new GlobalRunTimeException(ErrorCode.SYS_ERROR);
        }
        String param = sb.toString();
        Map<String, Object> result = new HashMap<>();
        log.info("请求头：{}", contentType);
        if (Objects.equals(contentType, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            String[] array = param.split(Constants.SPE3);
            for (String string : array) {
                String[] values = string.split(Constants.SPE4);
                result.put(values[0], values[1]);
            }
            return result;
        } else if (Objects.equals(contentType, MediaType.APPLICATION_JSON_VALUE)) {
            return JSONObject.parseObject(param, Map.class);
        } else {
            log.error("content type not support, {}", contentType);
            throw new GlobalRunTimeException("content type not support");
        }
    }

}
